import sys
import os
import threading
import time
import logging
import socket
import subprocess
import re
import queue
from datetime import datetime
import requests
import uiautomator2 as u2
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.keys import Keys  # Добавлено для Keys.CONTROL + 'a'
from selenium.common.exceptions import TimeoutException, NoSuchElementException, WebDriverException

from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, 
                             QPushButton, QLabel, QTextEdit, QLineEdit, QCheckBox, QComboBox,
                             QGridLayout, QGroupBox, QFormLayout, QMessageBox, QMenu, QFrame,
                             QScrollArea, QSizePolicy, QProgressBar)
from PyQt5.QtCore import Qt, QTimer, QThread, pyqtSignal, QObject, QMutex
from PyQt5.QtGui import QFont, QColor, QTextCursor, QCursor

import concurrent.futures  # Добавлено для ThreadPoolExecutor

# Настройка логирования
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(levelname)s - %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S'
)

# Глобальные переменные
successful_attempts = 0
failed_attempts = 0
batch_success_count = 0
pause_event = threading.Event()
pause_event.set()
is_processing = False
error_attempt_counter = 0
auto_paused_due_to_wifi = False

# Счетчики для различных типов ошибок
invalid_login_count = 0
captcha_count = 0
token_not_found_count = 0
timeout_count = 0
element_not_found_count = 0
unexpected_error_count = 0
sms_code_count = 0  # Новый счётчик для "Код из SMS"

all_results = []
file_lock = threading.Lock()
global_lock = threading.Lock()  # Добавлен для синхронизации глобальных счетчиков в многопотоке

# Очереди для логов
log_queue = queue.Queue()
token_queue = queue.Queue()
failed_queue = queue.Queue()

# Пути к файлам
tokens_file = r"tokens.txt"
failed_logins_file = r"неуспешные логи.txt"
login_pass_file = r"log pass.txt"
driver_path = r"chromedriver_win64\chromedriver.exe"
last_limit_file = r"last limit.txt"
last_threads_file = r"last_threads.txt"  # Новый файл для сохранения количества потоков
hide_browser_state_file = r"hide_browser_state.txt"
last_model_file = r"last_model.txt"
run_scenario_start_state_file = r"run_scenario_start_state.txt"
run_scenario_end_state_file = r"run_scenario_end_state.txt"
tokens_autoscroll_state_file = r"tokens_autoscroll_state.txt"
failed_autoscroll_state_file = r"failed_autoscroll_state.txt"
log_autoscroll_state_file = r"log_autoscroll_state.txt"  # Новый файл для состояния автопрокрутки логов

devices_by_model = {}
available_wifi_networks = []

def shorten_exception_message(e):
    """
    Возвращает только первую строку сообщения исключения для сокращения лога.
    """
    full_msg = str(e)
    lines = full_msg.splitlines()
    if lines:
        return lines[0].strip()  # Только первая строка, без лишних пробелов
    return full_msg  # Если нет строк, возвращаем как есть

def wait_for_login_button_then_click(driver, initial_timeout=5, retry_selector='.blue-button.text-button', retry_timeout=5, main_selector="button.btn[onclick='auth(2685278)']"):
    """
    Функция для ожидания основной кнопки авторизации с повторным поиском альтернативного селектора.
    
    Если основная кнопка не найдена в initial_timeout, пытаемся найти альтернативную кнопку по retry_selector,
    если она найдена, нажимаем и заново ищем основную кнопку в retry_timeout.
    """
    try:
        # Ждём основную кнопку
        WebDriverWait(driver, initial_timeout).until(
            EC.element_to_be_clickable((By.CSS_SELECTOR, main_selector))
        )
        return True
    except TimeoutException:
        # Основная кнопка не найдена, пробуем альтернативную
        try:
            alt_button = WebDriverWait(driver, retry_timeout).until(
                EC.element_to_be_clickable((By.CSS_SELECTOR, retry_selector))
            )
            alt_button.click()
            queue_log_message("Нажата альтернативная кнопка '.blue-button.text-button'.", "info")
            # После клика заново ищем основную кнопку
            WebDriverWait(driver, retry_timeout).until(
                EC.element_to_be_clickable((By.CSS_SELECTOR, main_selector))
            )
            return True
        except TimeoutException:
            # Ни основная, ни альтернативная кнопка не найдена
            return False

def get_short_error_message(e):
    """Извлекает краткое сообщение из исключения, без стека."""
    full_str = str(e)
    # Берем первую строку или релевантную часть
    short_msg = full_str.split('\n')[0].strip()  # Только первая строка
    if 'Message:' in short_msg:
        short_msg = short_msg.split('Message:')[1].strip()  # Удаляем 'Message:'
    return short_msg[:200]  # Ограничиваем длину, чтобы не засорять лог

# Функция для форматирования времени в формат ЧЧ:ММ:СС
def format_time(seconds):
    """Форматирует время из секунд в формат ЧЧ:ММ:СС"""
    hours = int(seconds // 3600)
    minutes = int((seconds % 3600) // 60)
    seconds = int(seconds % 60)
    return f"{hours:02d}:{minutes:02d}:{seconds:02d}"

# Сигналы для обновления GUI из потоков
class Signals(QObject):
    log_message = pyqtSignal(str, str)  # message, msg_type
    update_labels = pyqtSignal()
    append_token = pyqtSignal(str)
    append_failed_login = pyqtSignal(str, str)
    wifi_status_updated = pyqtSignal(str)
    ip_location_updated = pyqtSignal(str, str)
    device_scan_completed = pyqtSignal(list)
    wifi_scan_completed = pyqtSignal(list, str)
    update_progress = pyqtSignal(int, int)  # current, total

signals = Signals()

# Поток для обработки основных логов
class LogWorker(QThread):
    log_ready = pyqtSignal(str, str)
    
    def __init__(self):
        super().__init__()
        self.running = True
        
    def run(self):
        while self.running:
            try:
                message, msg_type = log_queue.get(timeout=0.1)
                self.log_ready.emit(message, msg_type)
                log_queue.task_done()
            except queue.Empty:
                continue
            except Exception as e:
                print(f"Ошибка в LogWorker: {e}")
                
    def stop(self):
        self.running = False

# Поток для обработки токенов
class TokenWorker(QThread):
    token_ready = pyqtSignal(str)
    
    def __init__(self):
        super().__init__()
        self.running = True
        
    def run(self):
        while self.running:
            try:
                token = token_queue.get(timeout=0.1)
                self.token_ready.emit(token)
                token_queue.task_done()
            except queue.Empty:
                continue
            except Exception as e:
                print(f"Ошибка в TokenWorker: {e}")
                
    def stop(self):
        self.running = False

# Поток для обработки неуспешных логинов
class FailedWorker(QThread):
    failed_ready = pyqtSignal(str, str)
    
    def __init__(self):
        super().__init__()
        self.running = True
        
    def run(self):
        while self.running:
            try:
                login, password = failed_queue.get(timeout=0.1)
                self.failed_ready.emit(login, password)
                failed_queue.task_done()
            except queue.Empty:
                continue
            except Exception as e:
                print(f"Ошибка в FailedWorker: {e}")
                
    def stop(self):
        self.running = False

# Функции для добавления сообщений в очереди
def queue_log_message(message, msg_type="info"):
    try:
        log_queue.put_nowait((message, msg_type))
    except queue.Full:
        pass  # Игнорируем если очередь переполнена

def queue_token_message(token):
    try:
        token_queue.put_nowait(token)
    except queue.Full:
        pass

def queue_failed_message(login, password):
    try:
        failed_queue.put_nowait((login, password))
    except queue.Full:
        pass

# Рабочий поток для обработки аккаунтов (теперь с поддержкой нескольких потоков)
class ProcessingThread(QThread):
    def __init__(self, main_window):
        super().__init__()
        self.main_window = main_window
        self.current_account = 0
        self.total_accounts = 0

    def run(self):
        global is_processing
        try:
            if self.main_window.run_scenario_start_check.isChecked():
                queue_log_message("Запуск сценария в начале.", "info")
                self.main_window.run_flight_mode_scenario()
            self.process_all_accounts()
        except Exception as ex:
            queue_log_message(f"Ошибка в основном потоке: {ex}", "error")
        finally:
            is_processing = False

    def process_all_accounts(self):
        global batch_success_count
        accounts = read_login_passwords()
        self.total_accounts = len(accounts)
        signals.update_progress.emit(0, self.total_accounts)
        
        processed_accounts = 0  # Счётчик фактически обработанных аккаунтов
        
        start = 0
        while start < len(accounts):
            pause_event.wait()
            if not is_processing:
                break
            
            # Получаем текущее значение потоков перед каждым батчем
            num_threads = self.main_window.get_current_threads()
            batch_size = num_threads
            batch = accounts[start:start + batch_size]
            
            with concurrent.futures.ThreadPoolExecutor(max_workers=len(batch)) as executor:
                futures = []
                for i, (login, password) in enumerate(batch):
                    idx = start + i + 1
                    futures.append(executor.submit(self.process_single_account, idx, login, password, self.total_accounts, i, is_retry=False))
                
                timed_out = []
                for future in concurrent.futures.as_completed(futures):
                    try:
                        status, account = future.result()
                        if status == 'timeout':
                            timed_out.append(account)
                        else:
                            # Увеличиваем счётчик только для окончательных результатов (не timeout)
                            processed_accounts += 1
                            signals.update_progress.emit(processed_accounts, self.total_accounts)
                    except Exception as e:
                        queue_log_message(f"Ошибка в потоке: {e}", "error")
                        processed_accounts += 1
                        signals.update_progress.emit(processed_accounts, self.total_accounts)
            
            # После завершения батча проверяем условия для изменения IP
            current_limit = self.main_window.get_limit_value()
            need_change = (len(timed_out) > 0) or (current_limit > 0 and batch_success_count >= current_limit)
            if need_change:
                queue_log_message("Изменение IP после завершения батча.", "info")
                self.main_window.run_flight_mode_scenario()
                batch_success_count = 0
            
            # Если были таймауты, делаем ретрай после изменения IP
            if timed_out:
                queue_log_message(f"Ретрай {len(timed_out)} аккаунтов после изменения IP.", "info")
                with concurrent.futures.ThreadPoolExecutor(max_workers=len(timed_out)) as executor:
                    retry_futures = []
                    for i, (login, password) in enumerate(timed_out):
                        idx = processed_accounts + i + 1  # Примерный индекс для лога
                        retry_futures.append(executor.submit(self.process_single_account, idx, login, password, self.total_accounts, i, is_retry=True))
                    
                    for future in concurrent.futures.as_completed(retry_futures):
                        try:
                            status, account = future.result()
                            # Теперь это окончательный результат, увеличиваем счётчик
                            processed_accounts += 1
                            signals.update_progress.emit(processed_accounts, self.total_accounts)
                        except Exception as e:
                            queue_log_message(f"Ошибка в ретрай-потоке: {e}", "error")
                            processed_accounts += 1
                            signals.update_progress.emit(processed_accounts, self.total_accounts)

            start += batch_size

        if is_processing and self.main_window.run_scenario_end_check.isChecked():
            queue_log_message("Запуск сценария в конце.", "info")
            self.main_window.run_flight_mode_scenario()

    def process_single_account(self, index, login, password, total, thread_index=0, is_retry=False):
        queue_log_message(f"Обработка аккаунта {index}/{total} в потоке {threading.get_ident()}: {login}{' (retry)' if is_retry else ''}", "info")
        status = self.main_window.open_browser(login, password, is_retry=is_retry, thread_index=thread_index)
        return status, (login, password)

class ClickableLabel(QLabel):
    """Кликабельная метка с подчёркиванием"""
    clicked = pyqtSignal()
    
    def __init__(self, text="", parent=None):
        super().__init__(text, parent)
        self.setCursor(QCursor(Qt.PointingHandCursor))
        self.setStyleSheet("""
            QLabel {
                color: #b8c0d0;
                text-decoration: underline;
                padding: 2px;
            }
            QLabel:hover {
                color: #ffffff;
                background-color: rgba(62, 70, 86, 0.3);
                border-radius: 2px;
            }
        """)
    
    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            self.clicked.emit()
        super().mousePressEvent(event)

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.processing_thread = None
        self.timer_start_time = None
        self.total_elapsed_time = 0
        self.is_timer_running = False
        
        # Переменные для автопрокрутки
        self.tokens_autoscroll_enabled = True
        self.failed_autoscroll_enabled = True
        self.log_autoscroll_enabled = True  # Новая переменная для автопрокрутки логов
        
        # Инициализация рабочих потоков для логов
        self.log_worker = LogWorker()
        self.token_worker = TokenWorker()
        self.failed_worker = FailedWorker()
        
        self.init_ui()
        self.position_window_right_half()
        self.setup_timers()
        self.connect_signals()
        self.setup_log_workers()
        self.load_settings()
        self.setup_context_menus()
        
    def setup_log_workers(self):
        """Настройка рабочих потоков для логов"""
        # Подключаем сигналы от рабочих потоков
        self.log_worker.log_ready.connect(self.log_message_direct)
        self.token_worker.token_ready.connect(self.append_token_direct)
        self.failed_worker.failed_ready.connect(self.append_failed_login_direct)
        
        # Запускаем рабочие потоки
        self.log_worker.start()
        self.token_worker.start()
        self.failed_worker.start()
        
    def position_window_right_half(self):
        """Размещение окна в правой половине экрана"""
        screen_geometry = QApplication.desktop().availableGeometry()
        screen_width = screen_geometry.width()
        screen_height = screen_geometry.height()
        
        window_width = 1200
        window_height = 950
        
        # Убедимся, что окно не выходит за пределы экрана
        if window_width > screen_width // 2:
            window_width = screen_width // 2
        
        # Вычисление позиции: центрируем в правой половине
        right_half_x = screen_width // 2
        x = right_half_x + (screen_width // 2 - window_width) // 2
        y = (screen_height - window_height) // 2
        
        self.resize(window_width, window_height)
        self.move(x, y)
        
    def init_ui(self):
        self.setWindowTitle("Получение токенов ВК (PyQt5)")
        self.setStyleSheet("""
            QMainWindow {
                background-color: #1e222a;
                color: #b8c0d0;
            }
            QGroupBox {
                font-weight: bold;
                border: 2px solid #3e4656;
                border-radius: 5px;
                margin-top: 1ex;
                padding-top: 10px;
                background-color: #262a33;
                color: #b8c0d0;
            }
            QGroupBox::title {
                subcontrol-origin: margin;
                left: 10px;
                padding: 0 5px 0 5px;
            }
            QPushButton {
                background-color: #3e4656;
                color: #b8c0d0;
                border: 1px solid #4e566a;
                padding: 8px;
                border-radius: 4px;
                font: 10pt "Segoe UI";
            }
            QPushButton:hover {
                background-color: #4e566a;
            }
            QPushButton:pressed {
                background-color: #2a2f38;
            }
            QPushButton:disabled {
                background-color: #2a2f38;
                color: #6b7280;
            }
            QLineEdit, QComboBox {
                background-color: #2a2f38;
                color: #b8c0d0;
                border: 1px solid #3e4656;
                padding: 5px;
                border-radius: 3px;
            }
            QTextEdit {
                background-color: #2a2f38;
                color: #b8c0d0;
                border: 1px solid #3e4656;
                border-radius: 3px;
            }
            QCheckBox {
                color: #b8c0d0;
            }
            QLabel {
                color: #b8c0d0;
            }
            QProgressBar {
                background-color: #2a2f38;
                border: 1px solid #3e4656;
                border-radius: 3px;
                text-align: center;
                color: #b8c0d0;
            }
            QProgressBar::chunk {
                background-color: #5b9bd5;
                border-radius: 2px;
            }
        """)
        
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        
        main_layout = QVBoxLayout(central_widget)
        main_layout.setSpacing(10)
        main_layout.setContentsMargins(10, 10, 10, 10)
        
        # Панель управления
        self.create_control_panel(main_layout)
        
        # Статистика
        self.create_status_panel(main_layout)
        
        # Прогресс бар
        self.create_progress_panel(main_layout)
        
        # Логи
        self.create_logs_panel(main_layout)
        
    def create_control_panel(self, parent_layout):
        control_group = QGroupBox("Панель управления")
        control_layout = QVBoxLayout(control_group)
        
        # Первая строка кнопок
        row1_layout = QHBoxLayout()
        
        self.start_button = QPushButton("Запустить")
        # Специальные стили для кнопки "Запустить" с динамическим hover
        self.start_button.setStyleSheet("""
            QPushButton {
                background-color: #5b9bd5;
                color: #b8c0d0;
                border: 1px solid #4e566a;
                padding: 8px;
                border-radius: 4px;
                font: 10pt "Segoe UI";
            }
            QPushButton:enabled:hover {
                background-color: #4a8bc2;
                border: 1px solid #5b9bd5;
                color: #ffffff;
            }
            QPushButton:enabled:pressed {
                background-color: #3a7ba8;
            }
            QPushButton:disabled {
                background-color: #2a2f38;
                color: #6b7280;
                border: 1px solid #3e4656;
            }
        """)
        self.start_button.clicked.connect(self.start_browser_thread)
        row1_layout.addWidget(self.start_button)
        
        self.pause_button = QPushButton("Приостановить")
        self.pause_button.setEnabled(False)
        self.pause_button.clicked.connect(self.pause_processing)
        row1_layout.addWidget(self.pause_button)
        
        self.resume_button = QPushButton("Восстановить")
        self.resume_button.setEnabled(False)
        self.resume_button.clicked.connect(self.resume_processing)
        row1_layout.addWidget(self.resume_button)
        
        self.restart_button = QPushButton("Перезапустить ошибки")
        self.restart_button.clicked.connect(self.restart_failed_accounts)
        row1_layout.addWidget(self.restart_button)
        
        self.update_labels_button = QPushButton("Обновить метки")
        self.update_labels_button.clicked.connect(self.update_status_labels)
        row1_layout.addWidget(self.update_labels_button)
        
        self.clear_logs_button = QPushButton("Очистить все журналы")
        self.clear_logs_button.clicked.connect(self.clear_all_logs)
        row1_layout.addWidget(self.clear_logs_button)
        
        row1_layout.addStretch()
        control_layout.addLayout(row1_layout)
        
        # Вторая строка - чекбоксы, лимит и количество потоков
        row2_layout = QHBoxLayout()
        
        self.hide_browser_check = QCheckBox("Скрыть браузер")
        row2_layout.addWidget(self.hide_browser_check)
        
        self.run_scenario_start_check = QCheckBox("Запустить сценарий в начале")
        row2_layout.addWidget(self.run_scenario_start_check)
        
        self.run_scenario_end_check = QCheckBox("Запустить сценарий в конце")
        row2_layout.addWidget(self.run_scenario_end_check)
        
        # Лимит токенов
        limit_label = QLabel("Лимит токенов:")
        row2_layout.addWidget(limit_label)
        
        self.limit_entry = QLineEdit()
        self.limit_entry.setMaximumWidth(60)
        self.limit_entry.setText("0")
        row2_layout.addWidget(self.limit_entry)
        
        # Новое: Количество потоков
        threads_label = QLabel("Количество потоков:")
        row2_layout.addWidget(threads_label)
        
        self.threads_entry = QLineEdit()
        self.threads_entry.setMaximumWidth(60)
        self.threads_entry.setText("1")  # По умолчанию 1 для стабильности
        self.threads_entry.textChanged.connect(self.save_last_threads)  # Сохранение в реальном времени при изменении
        row2_layout.addWidget(self.threads_entry)
        
        row2_layout.addStretch()
        control_layout.addLayout(row2_layout)
        
        # Третья строка - устройства и Wi-Fi
        row3_layout = QHBoxLayout()
        
        self.device_combobox = QComboBox()
        self.device_combobox.addItem("Нет подключённых устройств")
        self.device_combobox.currentTextChanged.connect(self.on_model_selected)
        row3_layout.addWidget(self.device_combobox)
        
        self.scan_devices_button = QPushButton("Сканировать устройства")
        self.scan_devices_button.clicked.connect(self.scan_devices_async)
        row3_layout.addWidget(self.scan_devices_button)
        
        wifi_label = QLabel("Выбрать Wi-Fi:")
        row3_layout.addWidget(wifi_label)
        
        self.wifi_combobox = QComboBox()
        self.wifi_combobox.setMinimumWidth(200)  # Устанавливаем минимальную ширину
        self.wifi_combobox.currentTextChanged.connect(self.on_wifi_selected)
        row3_layout.addWidget(self.wifi_combobox)
        
        self.refresh_wifi_button = QPushButton("Обновить Wi-Fi")
        self.refresh_wifi_button.clicked.connect(self.refresh_wifi_async)
        row3_layout.addWidget(self.refresh_wifi_button)
        
        row3_layout.addStretch()
        control_layout.addLayout(row3_layout)
        
        parent_layout.addWidget(control_group)
        
    def create_status_panel(self, parent_layout):
        status_group = QGroupBox("Статус")
        status_layout = QVBoxLayout(status_group)
        
        # Основная статистика
        stats_layout = QHBoxLayout()
        
        self.success_label = ClickableLabel("Успешные попытки: 0")
        self.success_label.setStyleSheet("""
            QLabel {
                color: #89ca78;
                text-decoration: underline;
                padding: 2px;
            }
            QLabel:hover {
                color: #a4d9a0;
                background-color: rgba(137, 202, 120, 0.2);
                border-radius: 2px;
            }
        """)
        self.success_label.clicked.connect(self.copy_pure_successful_tokens)
        stats_layout.addWidget(self.success_label)
        
        self.failure_label = ClickableLabel("Неуспешные попытки: 0")
        self.failure_label.setStyleSheet("""
            QLabel {
                color: #e57373;
                text-decoration: underline;
                padding: 2px;
            }
            QLabel:hover {
                color: #ffaaaa;
                background-color: rgba(229, 115, 115, 0.2);
                border-radius: 2px;
            }
        """)
        self.failure_label.clicked.connect(self.copy_failed_logins)
        stats_layout.addWidget(self.failure_label)
        
        self.count_label = QLabel("Всего аккаунтов: 0")
        self.count_label.setStyleSheet("color: #5b9bd5;")
        stats_layout.addWidget(self.count_label)
        
        self.remaining_label = QLabel("Оставшиеся аккаунты: 0")
        self.remaining_label.setStyleSheet("color: #f4a261;")
        stats_layout.addWidget(self.remaining_label)
        
        self.processed_label = ClickableLabel("Обработанные: 0")
        self.processed_label.setStyleSheet("""
            QLabel {
                color: #5b9bd5;
                text-decoration: underline;
                padding: 2px;
            }
            QLabel:hover {
                color: #7ab8e6;
                background-color: rgba(91, 155, 213, 0.2);
                border-radius: 2px;
            }
        """)
        self.processed_label.clicked.connect(self.copy_processed_count)
        stats_layout.addWidget(self.processed_label)
        
        copy_all_label = ClickableLabel("Copy all")
        copy_all_label.setStyleSheet("""
            QLabel {
                color: #b8c0d0;
                text-decoration: underline;
                padding: 2px;
            }
            QLabel:hover {
                color: #ffffff;
                background-color: rgba(184, 192, 208, 0.2);
                border-radius: 2px;
            }
        """)
        copy_all_label.clicked.connect(self.copy_all)
        stats_layout.addWidget(copy_all_label)
        
        copy_successful_label = ClickableLabel("Copy successful")
        copy_successful_label.setStyleSheet("""
            QLabel {
                color: #89ca78;
                text-decoration: underline;
                padding: 2px;
            }
            QLabel:hover {
                color: #a4d9a0;
                background-color: rgba(137, 202, 120, 0.2);
                border-radius: 2px;
            }
        """)
        copy_successful_label.clicked.connect(self.copy_successful_only)
        stats_layout.addWidget(copy_successful_label)
        
        stats_layout.addStretch()
        status_layout.addLayout(stats_layout)
        
        # Счетчики ошибок - первая строка (КЛИКАБЕЛЬНЫЕ)
        error_layout1 = QHBoxLayout()
        
        self.invalid_login_label = ClickableLabel("Невалидный логин/пароль: 0")
        self.invalid_login_label.setStyleSheet("""
            QLabel {
                color: #ff6b6b;
                font-size: 9pt;
                text-decoration: underline;
                padding: 2px;
            }
            QLabel:hover {
                color: #ff9999;
                background-color: rgba(255, 107, 107, 0.2);
                border-radius: 2px;
            }
        """)
        self.invalid_login_label.clicked.connect(self.copy_invalid_login_errors)
        error_layout1.addWidget(self.invalid_login_label)
        
        self.captcha_label = ClickableLabel("Капча: 0")
        self.captcha_label.setStyleSheet("""
            QLabel {
                color: #ff9f43;
                font-size: 9pt;
                text-decoration: underline;
                padding: 2px;
            }
            QLabel:hover {
                color: #ffb366;
                background-color: rgba(255, 159, 67, 0.2);
                border-radius: 2px;
            }
        """)
        self.captcha_label.clicked.connect(self.copy_captcha_errors)
        error_layout1.addWidget(self.captcha_label)
        
        self.token_not_found_label = ClickableLabel("Токен не найден: 0")
        self.token_not_found_label.setStyleSheet("""
            QLabel {
                color: #feca57;
                font-size: 9pt;
                text-decoration: underline;
                padding: 2px;
            }
            QLabel:hover {
                color: #ffd770;
                background-color: rgba(254, 202, 87, 0.2);
                border-radius: 2px;
            }
        """)
        self.token_not_found_label.clicked.connect(self.copy_token_not_found_errors)
        error_layout1.addWidget(self.token_not_found_label)
        
        # Новая метка для "Код из SMS"
        self.sms_code_label = ClickableLabel("Код из SMS: 0")
        self.sms_code_label.setStyleSheet("""
            QLabel {
                color: #ffcc00;
                font-size: 9pt;
                text-decoration: underline;
                padding: 2px;
            }
            QLabel:hover {
                color: #ffe066;
                background-color: rgba(255, 204, 0, 0.2);
                border-radius: 2px;
            }
        """)
        self.sms_code_label.clicked.connect(self.copy_sms_code_errors)
        error_layout1.addWidget(self.sms_code_label)
        
        error_layout1.addStretch()
        status_layout.addLayout(error_layout1)
        
        # Счетчики ошибок - вторая строка (КЛИКАБЕЛЬНЫЕ)
        error_layout2 = QHBoxLayout()
        
        self.timeout_label = ClickableLabel("Timeout: 0")
        self.timeout_label.setStyleSheet("""
            QLabel {
                color: #ff7675;
                font-size: 9pt;
                text-decoration: underline;
                padding: 2px;
            }
            QLabel:hover {
                color: #ff9999;
                background-color: rgba(255, 118, 117, 0.2);
                border-radius: 2px;
            }
        """)
        self.timeout_label.clicked.connect(self.copy_timeout_errors)
        error_layout2.addWidget(self.timeout_label)
        
        self.element_not_found_label = ClickableLabel("Элемент не найден: 0")
        self.element_not_found_label.setStyleSheet("""
            QLabel {
                color: #a29bfe;
                font-size: 9pt;
                text-decoration: underline;
                padding: 2px;
            }
            QLabel:hover {
                color: #b8b3ff;
                background-color: rgba(162, 155, 254, 0.2);
                border-radius: 2px;
            }
        """)
        self.element_not_found_label.clicked.connect(self.copy_element_not_found_errors)
        error_layout2.addWidget(self.element_not_found_label)
        
        self.unexpected_error_label = ClickableLabel("Неожиданные ошибки: 0")
        self.unexpected_error_label.setStyleSheet("""
            QLabel {
                color: #fd79a8;
                font-size: 9pt;
                text-decoration: underline;
                padding: 2px;
            }
            QLabel:hover {
                color: #ff99cc;
                background-color: rgba(253, 121, 168, 0.2);
                border-radius: 2px;
            }
        """)
        self.unexpected_error_label.clicked.connect(self.copy_unexpected_errors)
        error_layout2.addWidget(self.unexpected_error_label)
        
        error_layout2.addStretch()
        status_layout.addLayout(error_layout2)
        
        # Дополнительная информация
        info_layout = QHBoxLayout()
        
        self.total_time_label = QLabel("Общее время: 00:00:00")
        self.total_time_label.setStyleSheet("color: #f4a261;")
        info_layout.addWidget(self.total_time_label)
        
        self.wifi_status_label = QLabel("Подключенный Wi-Fi: Неизвестно")
        self.wifi_status_label.setStyleSheet("color: #bb77d1;")
        info_layout.addWidget(self.wifi_status_label)
        
        self.ip_label = QLabel("IP: Сканирование...")
        info_layout.addWidget(self.ip_label)
        
        self.location_label = QLabel("Локация: Сканирование...")
        info_layout.addWidget(self.location_label)
        
        info_layout.addStretch()
        status_layout.addLayout(info_layout)
        
        parent_layout.addWidget(status_group)
        
    def create_progress_panel(self, parent_layout):
        """Создание панели прогресса"""
        progress_group = QGroupBox("Прогресс выполнения")
        progress_layout = QVBoxLayout(progress_group)
        
        # Основной прогресс бар
        self.progress_bar = QProgressBar()
        self.progress_bar.setMinimum(0)
        self.progress_bar.setMaximum(100)
        self.progress_bar.setValue(0)
        self.progress_bar.setFormat("Ожидание запуска...")
        progress_layout.addWidget(self.progress_bar)
        
        # Информация о прогрессе
        progress_info_layout = QHBoxLayout()
        
        self.progress_label = QLabel("Готов к запуску")
        self.progress_label.setStyleSheet("color: #b8c0d0;")
        progress_info_layout.addWidget(self.progress_label)
        
        progress_info_layout.addStretch()
        progress_layout.addLayout(progress_info_layout)
        
        parent_layout.addWidget(progress_group)
        
    def create_logs_panel(self, parent_layout):
        logs_layout = QHBoxLayout()
        
        # Журнал действий
        log_group = QGroupBox("Журнал действий")
        log_layout = QVBoxLayout(log_group)
        self.log_text = QTextEdit()
        self.log_text.setReadOnly(True)
        self.log_text.setMinimumHeight(300)
        log_layout.addWidget(self.log_text)
        logs_layout.addWidget(log_group)
        
        # Успешные токены
        tokens_group = QGroupBox("Успешные токены")
        tokens_layout = QVBoxLayout(tokens_group)
        
        self.successful_tokens_text = QTextEdit()
        self.successful_tokens_text.setReadOnly(True)
        self.successful_tokens_text.setMinimumHeight(300)
        tokens_layout.addWidget(self.successful_tokens_text)
        logs_layout.addWidget(tokens_group)
        
        parent_layout.addLayout(logs_layout)
        
        # Неуспешные логины
        failed_group = QGroupBox("Неуспешные логины и пароли")
        failed_layout = QVBoxLayout(failed_group)
        
        self.failed_logins_text = QTextEdit()
        self.failed_logins_text.setReadOnly(True)
        self.failed_logins_text.setMaximumHeight(150)
        failed_layout.addWidget(self.failed_logins_text)
        
        parent_layout.addWidget(failed_group)
        
    def setup_timers(self):
        # Таймер для обновления времени работы
        self.runtime_timer = QTimer()
        self.runtime_timer.timeout.connect(self.update_runtime_display)
        
        # Таймер для проверки Wi-Fi
        self.wifi_timer = QTimer()
        self.wifi_timer.timeout.connect(self.update_wifi_status)
        self.wifi_timer.start(2000)  # Каждые 2 секунды
        
        # Таймер для обновления IP и локации
        self.ip_timer = QTimer()
        self.ip_timer.timeout.connect(self.update_ip_location_display)
        self.ip_timer.start(15000)  # Каждые 15 секунд
        
    def setup_context_menus(self):
        # Контекстное меню для логов
        self.log_text.setContextMenuPolicy(Qt.CustomContextMenu)
        self.log_text.customContextMenuRequested.connect(self.show_log_context_menu)
        
        self.successful_tokens_text.setContextMenuPolicy(Qt.CustomContextMenu)
        self.successful_tokens_text.customContextMenuRequested.connect(self.show_tokens_context_menu)
        
        self.failed_logins_text.setContextMenuPolicy(Qt.CustomContextMenu)
        self.failed_logins_text.customContextMenuRequested.connect(self.show_failed_context_menu)
        
    def connect_signals(self):
        signals.log_message.connect(self.queue_log_message_wrapper)
        signals.update_labels.connect(self.update_status_labels)
        signals.append_token.connect(self.queue_token_wrapper)
        signals.append_failed_login.connect(self.queue_failed_wrapper)
        signals.wifi_status_updated.connect(self.wifi_status_label.setText)
        signals.ip_location_updated.connect(self.update_ip_location_labels)
        signals.device_scan_completed.connect(self.update_device_list)
        signals.wifi_scan_completed.connect(self.update_wifi_list)
        signals.update_progress.connect(self.update_progress_bar)
        
    def update_progress_bar(self, current, total):
        """Обновление прогресс бара"""
        if total > 0:
            percentage = int((current / total) * 100)
            self.progress_bar.setValue(percentage)
            self.progress_bar.setFormat(f"{current}/{total} ({percentage}%)")
            self.progress_label.setText(f"Обработано: {current} из {total} аккаунтов")
        else:
            self.progress_bar.setValue(0)
            self.progress_bar.setFormat("Ожидание...")
            self.progress_label.setText("Готов к запуску")
        
    def queue_log_message_wrapper(self, message, msg_type):
        """Обертка для добавления лог-сообщений в очередь"""
        queue_log_message(message, msg_type)
        
    def queue_token_wrapper(self, token):
        """Обертка для добавления токенов в очередь"""
        queue_token_message(token)
        
    def queue_failed_wrapper(self, login, password):
        """Обертка для добавления неуспешных логинов в очередь"""
        queue_failed_message(login, password)
        
    def log_message_direct(self, message, msg_type="info"):
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        full_message = f"[{timestamp}] {message}"
    
        # Создаём новый курсор для вставки (не трогаем текущий видимый курсор)
        cursor = QTextCursor(self.log_text.document())
        cursor.movePosition(QTextCursor.End)
    
        # Устанавливаем цвет текста
        if msg_type == "info":
            self.log_text.setTextColor(QColor("#b8c0d0"))
        elif msg_type == "success":
            self.log_text.setTextColor(QColor("#89ca78"))
        elif msg_type == "error":
            self.log_text.setTextColor(QColor("#e57373"))
    
        # Вставляем текст через курсор (без setTextCursor, чтобы не прокручивать автоматически)
        cursor.insertText(full_message + "\n")
    
        # Прокручиваем только если автопрокрутка включена
        if self.log_autoscroll_enabled:
            visible_cursor = self.log_text.textCursor()
            visible_cursor.movePosition(QTextCursor.End)
            self.log_text.setTextCursor(visible_cursor)
            self.log_text.ensureCursorVisible()
            
    def append_token_direct(self, token):
        """Прямое добавление токена (вызывается из рабочего потока)"""
        self.successful_tokens_text.append(token)
        if self.tokens_autoscroll_enabled:
            self.successful_tokens_text.ensureCursorVisible()
        
    def append_failed_login_direct(self, login, password):
        """Прямое добавление неуспешного логина (вызывается из рабочего потока)"""
        self.failed_logins_text.append(f"{login}:{password}")
        if self.failed_autoscroll_enabled:
            self.failed_logins_text.ensureCursorVisible()
            
    def load_settings(self):
        # Загрузка сохраненных настроек
        self.hide_browser_check.setChecked(self.load_hide_browser_state())
        self.run_scenario_start_check.setChecked(self.load_run_scenario_start_state())
        self.run_scenario_end_check.setChecked(self.load_run_scenario_end_state())
        self.limit_entry.setText(self.load_last_limit())
        self.threads_entry.setText(self.load_last_threads())  # Загрузка количества потоков
        
        # Загрузка состояния автопрокрутки
        self.tokens_autoscroll_enabled = self.load_tokens_autoscroll_state()
        self.failed_autoscroll_enabled = self.load_failed_autoscroll_state()
        self.log_autoscroll_enabled = self.load_log_autoscroll_state()  # Новая загрузка для логов
        
        # Инициализация
        queue_log_message("Журнал действий:", "info")
        self.update_status_labels()
        self.scan_devices_async()
        self.refresh_wifi_async()
        self.update_ip_location_display()
        
    def reset_timer(self):
        """Сброс таймера до нуля"""
        self.total_elapsed_time = 0
        self.timer_start_time = None
        self.is_timer_running = False
        self.runtime_timer.stop()
        self.total_time_label.setText("Общее время: 00:00:00")
        queue_log_message("Таймер сброшен", "info")
        
    def update_status_labels(self):
        accounts = read_login_passwords()
        total_accounts = len(accounts)
        remaining = total_accounts - (successful_attempts + failed_attempts)
        processed = successful_attempts + failed_attempts
        
        self.success_label.setText(f"Успешные попытки: {successful_attempts}")
        self.failure_label.setText(f"Неуспешные попытки: {failed_attempts}")
        self.count_label.setText(f"Всего аккаунтов: {total_accounts}")
        self.remaining_label.setText(f"Оставшиеся аккаунты: {remaining}")
        self.processed_label.setText(f"Обработанные: {processed}")
        
        # Обновляем счетчики ошибок
        self.invalid_login_label.setText(f"Невалидный логин/пароль: {invalid_login_count}")
        self.captcha_label.setText(f"Капча: {captcha_count}")
        self.token_not_found_label.setText(f"Токен не найден: {token_not_found_count}")
        self.timeout_label.setText(f"Timeout: {timeout_count}")
        self.element_not_found_label.setText(f"Элемент не найден: {element_not_found_count}")
        self.unexpected_error_label.setText(f"Неожиданные ошибки: {unexpected_error_count}")
        self.sms_code_label.setText(f"Код из SMS: {sms_code_count}")  # Новая метка
        
    def ensure_wifi_connection(self):
        selected_wifi = self.wifi_combobox.currentText()
        current_wifi = get_connected_wifi()
        
        if selected_wifi and selected_wifi != current_wifi:
            queue_log_message(f"Текущая Wi-Fi сеть '{current_wifi}' не совпадает с выбранной '{selected_wifi}'. Пытаемся подключиться...", "info")
            if connect_to_wifi(selected_wifi):
                queue_log_message(f"Успешно подключено к '{selected_wifi}'.", "success")
                return True
            else:
                queue_log_message(f"Не удалось подключиться к '{selected_wifi}'.", "error")
                return False
        return True
        
    def get_current_threads(self):
        """Получение текущего значения количества потоков"""
        try:
            return max(1, int(self.threads_entry.text()))
        except ValueError:
            return 1
        
    def start_browser_thread(self):
        global is_processing
        if is_processing:
            QMessageBox.information(self, "Информация", "Процесс уже запущен.")
            return
            
        if not self.ensure_wifi_connection():
            QMessageBox.warning(self, "Внимание", 
                               "Не удалось подключиться к выбранной Wi-Fi сети.\nЗапуск невозможен.")
            return
            
        add_token_separator()
        
        self.save_last_limit()
        self.save_last_threads()  # Сохранение количества потоков при запуске
        is_processing = True
        pause_event.set()
        
        self.start_button.setEnabled(False)
        self.pause_button.setEnabled(True)
        self.resume_button.setEnabled(False)
        
        # Сброс прогресс бара
        self.progress_bar.setValue(0)
        self.progress_bar.setFormat("Инициализация...")
        self.progress_label.setText("Подготовка к запуску...")
        # *** ДОБАВЛЕНО: Сброс таймера только при запуске новой сессии ***
        self.reset_timer()
        
        # Запуск таймера
        self.start_timer()
        
        queue_log_message(f"Запуск обработки.", "info")
        
        # Запуск обработки в отдельном потоке
        self.processing_thread = ProcessingThread(self)
        self.processing_thread.finished.connect(self.on_processing_finished)
        self.processing_thread.start()
        
    def pause_processing(self):
        global auto_paused_due_to_wifi
        if not is_processing:
            return
        pause_event.clear()
        auto_paused_due_to_wifi = False
        queue_log_message("Процесс приостановлен (вручную).", "info")
        self.pause_button.setEnabled(False)
        self.resume_button.setEnabled(True)
        self.stop_timer()
        
    def resume_processing(self):
        if not is_processing:
            return
        if not self.ensure_wifi_connection():
            QMessageBox.warning(self, "Внимание", 
                               "Невозможно возобновить: не удалось подключиться к выбранной Wi-Fi сети.")
            return
        pause_event.set()
        queue_log_message("Процесс восстановлен (вручную).", "info")
        self.pause_button.setEnabled(True)
        self.resume_button.setEnabled(False)
        self.start_timer()
        
    def on_processing_finished(self):
        global is_processing
        is_processing = False
        self.start_button.setEnabled(True)
        self.pause_button.setEnabled(False)
        self.resume_button.setEnabled(False)
        self.stop_timer()
        
        # Завершение прогресс бара
        self.progress_bar.setFormat("Завершено")
        self.progress_label.setText("Обработка завершена")
        
    def restart_failed_accounts(self):
        global successful_attempts, failed_attempts, batch_success_count
        global invalid_login_count, captcha_count, token_not_found_count
        global timeout_count, element_not_found_count, unexpected_error_count, sms_code_count
        
        # Сброс всех счетчиков
        successful_attempts = 0
        failed_attempts = 0
        batch_success_count = 0
        invalid_login_count = 0
        captcha_count = 0
        token_not_found_count = 0
        timeout_count = 0
        element_not_found_count = 0
        unexpected_error_count = 0
        sms_code_count = 0  # Сброс нового счётчика
        
        all_results.clear()
        self.update_status_labels()
        self.successful_tokens_text.clear()
        
        content = self.failed_logins_text.toPlainText().strip()
        if not content:
            QMessageBox.information(self, "Информация", "Нет аккаунтов для перезапуска.")
            return
            
        self.failed_logins_text.clear()
        
        try:
            with open(failed_logins_file, "w", encoding="utf-8") as f:
                f.write("")
        except Exception as e:
            queue_log_message(f"Ошибка при очистке файла неуспешных логинов: {e}", "error")
            
        def process_failed():
            lines = content.splitlines()
            for line in lines:
                parts = line.split(":", 1)
                if len(parts) == 2:
                    login, password = parts
                    queue_log_message(f"Перезапуск аккаунта: {login}", "info")
                    self.open_browser(login, password)
            signals.update_labels.emit()
            
        thread = threading.Thread(target=process_failed, daemon=True)
        thread.start()
        
    def clear_all_logs(self):
        reply = QMessageBox.question(self, "Подтверждение",
                                     "Вы уверены, что хотите очистить журналы в интерфейсе?\n(Файлы tokens.txt и неуспешные логи.txt не будут затронуты)",
                                     QMessageBox.Yes | QMessageBox.No)
        if reply == QMessageBox.Yes:
            try:
                self.log_text.clear()
                self.successful_tokens_text.clear()
                self.failed_logins_text.clear()
            
                global successful_attempts, failed_attempts
                global invalid_login_count, captcha_count, token_not_found_count
                global timeout_count, element_not_found_count, unexpected_error_count, sms_code_count
            
                successful_attempts = 0
                failed_attempts = 0
                invalid_login_count = 0
                captcha_count = 0
                token_not_found_count = 0
                timeout_count = 0
                element_not_found_count = 0
                unexpected_error_count = 0
                sms_code_count = 0  # Сброс нового счётчика
                all_results.clear()
                self.update_status_labels()
                # *** ИЗМЕНЕНО: Таймер не сбрасывается при очистке логов, только если он не работает ***
                if not self.is_timer_running:
                    self.reset_timer()
                
                # Сброс прогресс бара
                self.progress_bar.setValue(0)
                self.progress_bar.setFormat("Ожидание запуска...")
                self.progress_label.setText("Готов к запуску")
                
                queue_log_message("Журналы интерфейса очищены (файлы tokens.txt и неуспешные логи.txt сохранены).", "info")
            except Exception as e:
                QMessageBox.critical(self, "Ошибка", f"Не удалось очистить журналы: {e}")
        
    def copy_pure_successful_tokens(self):
        """Копирование только чистых успешных токенов (без ошибок)"""
        if not all_results:
            queue_log_message("Нет данных для копирования успешных токенов.", "info")
            return
            
        error_keywords = [
            "Невалидный логин или пароль",
            "Найдена капча", 
            "Токен не найден",
            "Timeout",
            "Элемент не найден",
            "Ошибка:",
            "Неожиданная ошибка",
            "Неизвестная ошибка",
            "Код из SMS"  # Добавлено для новой ошибки
        ]
        
        successful_tokens = []
        for login, password, result in all_results:
            is_error = any(keyword in result for keyword in error_keywords)
            # Проверяем что это токен (длинный и без ошибок)
            if not is_error and len(result) > 30 and ":" not in result:
                successful_tokens.append(result)
                
        if not successful_tokens:
            queue_log_message("Нет чистых успешных токенов для копирования.", "info")
            return
            
        text = "\n".join(successful_tokens)
        QApplication.clipboard().setText(text)
        queue_log_message(f"Скопировано {len(successful_tokens)} чистых успешных токенов в буфер обмена.", "info")
        
    def copy_failed_logins(self):
        content = self.failed_logins_text.toPlainText().strip()
        if not content:
            QMessageBox.information(self, "Информация", "Нет неуспешных логинов для копирования.")
            return
        QApplication.clipboard().setText(content)
        queue_log_message("Неуспешные логины и пароли скопированы в буфер обмена.", "info")
        
    def copy_all(self):
        if not all_results:
            QMessageBox.information(self, "Информация", "Нет данных для копирования.")
            return
            
        lines = [f"{login}:{password}\t{result}" for login, password, result in all_results]
        text = "\n".join(lines)
        QApplication.clipboard().setText(text)
        queue_log_message("Список логин:пароль и результатов скопирован.", "info")
        
    def copy_successful_only(self):
        if not all_results:
            QMessageBox.information(self, "Информация", "Нет данных для копирования.")
            return
            
        error_keywords = [
            "Невалидный логин или пароль",
            "Найдена капча", 
            "Токен не найден",
            "Timeout",
            "Элемент не найден",
            "Ошибка:",
            "Неожиданная ошибка",
            "Неизвестная ошибка",
            "Код из SMS"  # Добавлено для новой ошибки
        ]
        
        successful_results = []
        for login, password, result in all_results:
            is_error = any(keyword in result for keyword in error_keywords)
            if not is_error and len(result) > 10:
                successful_results.append((login, password, result))
                
        if not successful_results:
            QMessageBox.information(self, "Информация", "Нет успешных результатов для копирования.")
            return
            
        lines = [f"{login}:{password}\t{result}" for login, password, result in successful_results]
        text = "\n".join(lines)
        QApplication.clipboard().setText(text)
        queue_log_message(f"Скопировано {len(successful_results)} успешных результатов в буфер обмена.", "info")
        
    def copy_processed_count(self):
        """Копирование количества обработанных аккаунтов"""
        processed = successful_attempts + failed_attempts
        QApplication.clipboard().setText(str(processed))
        queue_log_message(f"Скопировано количество обработанных аккаунтов: {processed}", "info")
        
    # МЕТОДЫ ДЛЯ КОПИРОВАНИЯ ОШИБОК ПО ТИПАМ
    def copy_invalid_login_errors(self):
        """Копирование ошибок невалидного логина/пароля"""
        if not all_results:
            queue_log_message("Нет данных для копирования ошибок невалидного логина.", "info")
            return
            
        invalid_results = []
        for login, password, result in all_results:
            if "Невалидный логин или пароль" in result:
                invalid_results.append(f"{login}:{password}\t{result}")
                
        if not invalid_results:
            queue_log_message("Нет ошибок невалидного логина/пароля для копирования.", "info")
            return
            
        text = "\n".join(invalid_results)
        QApplication.clipboard().setText(text)
        queue_log_message(f"Скопировано {len(invalid_results)} ошибок невалидного логина/пароля.", "info")
        
    def copy_captcha_errors(self):
        """Копирование ошибок капчи"""
        if not all_results:
            queue_log_message("Нет данных для копирования ошибок капчи.", "info")
            return
            
        captcha_results = []
        for login, password, result in all_results:
            if "Найдена капча" in result or "капча" in result.lower():
                captcha_results.append(f"{login}:{password}\t{result}")
                
        if not captcha_results:
            queue_log_message("Нет ошибок капчи для копирования.", "info")
            return
            
        text = "\n".join(captcha_results)
        QApplication.clipboard().setText(text)
        queue_log_message(f"Скопировано {len(captcha_results)} ошибок капчи.", "info")
        
    def copy_token_not_found_errors(self):
        """Копирование ошибок 'токен не найден'"""
        if not all_results:
            queue_log_message("Нет данных для копирования ошибок 'токен не найден'.", "info")
            return
            
        token_not_found_results = []
        for login, password, result in all_results:
            if "Токен не найден" in result:
                token_not_found_results.append(f"{login}:{password}\t{result}")
                
        if not token_not_found_results:
            queue_log_message("Нет ошибок 'токен не найден' для копирования.", "info")
            return
            
        text = "\n".join(token_not_found_results)
        QApplication.clipboard().setText(text)
        queue_log_message(f"Скопировано {len(token_not_found_results)} ошибок 'токен не найден'.", "info")
        
    def copy_timeout_errors(self):
        """Копирование ошибок timeout"""
        if not all_results:
            queue_log_message("Нет данных для копирования ошибок timeout.", "info")
            return
            
        timeout_results = []
        for login, password, result in all_results:
            if "Timeout" in result:
                timeout_results.append(f"{login}:{password}\t{result}")
                
        if not timeout_results:
            queue_log_message("Нет ошибок timeout для копирования.", "info")
            return
            
        text = "\n".join(timeout_results)
        QApplication.clipboard().setText(text)
        queue_log_message(f"Скопировано {len(timeout_results)} ошибок timeout.", "info")
        
    def copy_element_not_found_errors(self):
        """Копирование ошибок 'элемент не найден'"""
        if not all_results:
            queue_log_message("Нет данных для копирования ошибок 'элемент не найден'.", "info")
            return
            
        element_not_found_results = []
        for login, password, result in all_results:
            if "Элемент не найден" in result:
                element_not_found_results.append(f"{login}:{password}\t{result}")
                
        if not element_not_found_results:
            queue_log_message("Нет ошибок 'элемент не найден' для копирования.", "info")
            return
            
        text = "\n".join(element_not_found_results)
        QApplication.clipboard().setText(text)
        queue_log_message(f"Скопировано {len(element_not_found_results)} ошибок 'элемент не найден'.", "info")
        
    def copy_unexpected_errors(self):
        """Копирование неожиданных ошибок"""
        if not all_results:
            queue_log_message("Нет данных для копирования неожиданных ошибок.", "info")
            return
            
        unexpected_results = []
        for login, password, result in all_results:
            if ("Ошибка:" in result or "Неожиданная ошибка" in result or "Неизвестная ошибка" in result) and \
               "Невалидный логин или пароль" not in result and \
               "Найдена капча" not in result and \
               "Токен не найден" not in result and \
               "Timeout" not in result and \
               "Элемент не найден" not in result and \
               "Код из SMS" not in result:
                unexpected_results.append(f"{login}:{password}\t{result}")
                
        if not unexpected_results:
            queue_log_message("Нет неожиданных ошибок для копирования.", "info")
            return
            
        text = "\n".join(unexpected_results)
        QApplication.clipboard().setText(text)
        queue_log_message(f"Скопировано {len(unexpected_results)} неожиданных ошибок.", "info")
        
    def copy_sms_code_errors(self):
        """Копирование ошибок 'Код из SMS' (новый метод)"""
        if not all_results:
            queue_log_message("Нет данных для копирования ошибок 'Код из SMS'.", "info")
            return
            
        sms_results = []
        for login, password, result in all_results:
            if "Код из SMS" in result:
                sms_results.append(f"{login}:{password}\t{result}")
                
        if not sms_results:
            queue_log_message("Нет ошибок 'Код из SMS' для копирования.", "info")
            return
            
        text = "\n".join(sms_results)
        QApplication.clipboard().setText(text)
        queue_log_message(f"Скопировано {len(sms_results)} ошибок 'Код из SMS'.", "info")
        
    def show_log_context_menu(self, pos):
        menu = QMenu()
        copy_action = menu.addAction("Копировать")
        select_all_action = menu.addAction("Выбрать всё")
        menu.addSeparator()
        autoscroll_action = menu.addAction("Автопрокрутка")
        autoscroll_action.setCheckable(True)
        autoscroll_action.setChecked(self.log_autoscroll_enabled)  # Устанавливаем текущее состояние
        
        action = menu.exec_(self.log_text.mapToGlobal(pos))
        if action == copy_action:
            cursor = self.log_text.textCursor()
            if cursor.hasSelection():
                selected_text = cursor.selectedText().replace('\u2029', '\n')
                QApplication.clipboard().setText(selected_text)
        elif action == select_all_action:
            self.log_text.selectAll()
        elif action == autoscroll_action:
            # Обработчик для переключения автопрокрутки логов
            self.log_autoscroll_enabled = not self.log_autoscroll_enabled
            self.save_log_autoscroll_state()
            if self.log_autoscroll_enabled:
                queue_log_message("Автопрокрутка для журнала действий включена.", "info")
            else:
                queue_log_message("Автопрокрутка для журнала действий отключена.", "info")
            
    def show_tokens_context_menu(self, pos):
        menu = QMenu()
        copy_action = menu.addAction("Копировать")
        select_all_action = menu.addAction("Выбрать всё")
        menu.addSeparator()
        autoscroll_action = menu.addAction("Автопрокрутка")
        autoscroll_action.setCheckable(True)
        autoscroll_action.setChecked(self.tokens_autoscroll_enabled)
        
        action = menu.exec_(self.successful_tokens_text.mapToGlobal(pos))
        if action == copy_action:
            cursor = self.successful_tokens_text.textCursor()
            if cursor.hasSelection():
                selected_text = cursor.selectedText().replace('\u2029', '\n')
                QApplication.clipboard().setText(selected_text)
        elif action == select_all_action:
            self.successful_tokens_text.selectAll()
        elif action == autoscroll_action:
            self.tokens_autoscroll_enabled = not self.tokens_autoscroll_enabled
            self.save_tokens_autoscroll_state()
            if self.tokens_autoscroll_enabled:
                queue_log_message("Автопрокрутка для токенов включена.", "info")
            else:
                queue_log_message("Автопрокрутка для токенов отключена.", "info")
                
    def show_failed_context_menu(self, pos):
        menu = QMenu()
        copy_action = menu.addAction("Копировать")
        select_all_action = menu.addAction("Выбрать всё")
        menu.addSeparator()
        autoscroll_action = menu.addAction("Автопрокрутка")
        autoscroll_action.setCheckable(True)
        autoscroll_action.setChecked(self.failed_autoscroll_enabled)
        
        action = menu.exec_(self.failed_logins_text.mapToGlobal(pos))
        if action == copy_action:
            cursor = self.failed_logins_text.textCursor()
            if cursor.hasSelection():
                selected_text = cursor.selectedText().replace('\u2029', '\n')
                QApplication.clipboard().setText(selected_text)
        elif action == select_all_action:
            self.failed_logins_text.selectAll()
        elif action == autoscroll_action:
            self.failed_autoscroll_enabled = not self.failed_autoscroll_enabled
            self.save_failed_autoscroll_state()
            if self.failed_autoscroll_enabled:
                queue_log_message("Автопрокрутка для неуспешных логинов включена.", "info")
            else:
                queue_log_message("Автопрокрутка для неуспешных логинов отключена.", "info")
            
    def start_timer(self):
        self.timer_start_time = time.time()
        self.is_timer_running = True
        self.runtime_timer.start(1000)  # Обновление каждую секунду
        
    def stop_timer(self):
        if self.is_timer_running and self.timer_start_time:
            self.total_elapsed_time += time.time() - self.timer_start_time
            self.is_timer_running = False
            self.runtime_timer.stop()
            
    def update_runtime_display(self):
        """Обновление отображения времени в формате ЧЧ:ММ:СС"""
        if self.is_timer_running and self.timer_start_time:
            current_time = self.total_elapsed_time + (time.time() - self.timer_start_time)
            formatted_time = format_time(current_time)
            self.total_time_label.setText(f"Время выполнения: {formatted_time}")
        else:
            formatted_time = format_time(self.total_elapsed_time)
            self.total_time_label.setText(f"Общее время: {formatted_time}")
            
    def update_wifi_status(self):
        current_wifi = get_connected_wifi()
        self.wifi_status_label.setText(f"Подключенный Wi-Fi: {current_wifi}")
        
        # Логика автопаузы при смене Wi-Fi
        selected_wifi = self.wifi_combobox.currentText()
        global auto_paused_due_to_wifi
        
        if selected_wifi and selected_wifi != current_wifi:
            if is_processing and pause_event.is_set():
                pause_event.clear()
                auto_paused_due_to_wifi = True
                queue_log_message("Процесс приостановлен автоматически (Wi-Fi mismatch).", "info")
            if not is_processing:
                self.start_button.setEnabled(False)
        else:
            if not is_processing:
                self.start_button.setEnabled(True)
            else:
                if auto_paused_due_to_wifi and not pause_event.is_set():
                    pause_event.set()
                    auto_paused_due_to_wifi = False
                    queue_log_message("Процесс возобновлён автоматически (Wi-Fi соответствует).", "info")
                    
    def update_ip_location_display(self):
        def worker():
            ip = get_external_ip()
            if ip != "Недоступно":
                location = get_location_by_ip(ip)
            else:
                location = "Недоступно"
            signals.ip_location_updated.emit(ip, location)
            
        thread = threading.Thread(target=worker, daemon=True)
        thread.start()
        
    def update_ip_location_labels(self, ip, location):
        self.ip_label.setText(f"IP: {ip}")
        self.location_label.setText(f"Локация: {location}")
        
    def scan_devices_async(self):
        def worker():
            global devices_by_model
            devices_by_model.clear()
            try:
                result = subprocess.run(["adb", "devices", "-l"], 
                                      capture_output=True, text=True, check=False)
                output = result.stdout.strip().splitlines()
                for line in output[1:]:
                    line = line.strip()
                    if not line or "offline" in line or "unauthorized" in line or "unknown" in line:
                        continue
                    parts = line.split()
                    serial = parts[0]
                    model = None
                    for p in parts:
                        if p.startswith("model:"):
                            model = p.split(":", 1)[1]
                            break
                    if not model:
                        model = "UnknownModel"
                    devices_by_model[model] = serial
                queue_log_message(f"Устройства обновлены: {devices_by_model}", "info")
            except Exception as e:
                queue_log_message(f"Ошибка при сканировании устройств: {e}", "error")
            
            model_list = list(devices_by_model.keys())
            signals.device_scan_completed.emit(model_list)
            
        thread = threading.Thread(target=worker, daemon=True)
        thread.start()
        
    def update_device_list(self, model_list):
        current_selection = self.load_last_model()
        self.device_combobox.clear()
        
        if model_list:
            self.device_combobox.addItems(model_list)
            if current_selection in model_list:
                self.device_combobox.setCurrentText(current_selection)
        else:
            self.device_combobox.addItem("Нет подключённых устройств")
            
    def refresh_wifi_async(self):
        def worker():
            global available_wifi_networks
            available_wifi_networks = get_available_networks()
            current_ssid = get_connected_wifi()
            signals.wifi_scan_completed.emit(available_wifi_networks, current_ssid)
            
        thread = threading.Thread(target=worker, daemon=True)
        thread.start()
        
    def update_wifi_list(self, networks, current_ssid):
        self.wifi_combobox.clear()
        if networks:
            self.wifi_combobox.addItems(networks)
            if current_ssid in networks:
                self.wifi_combobox.setCurrentText(current_ssid)
                
    def on_model_selected(self):
        model = self.device_combobox.currentText()
        self.save_last_model(model)
        
    def on_wifi_selected(self):
        global auto_paused_due_to_wifi
        selected = self.wifi_combobox.currentText()
        current_wifi = get_connected_wifi()
        if selected and selected != current_wifi:
            if is_processing and pause_event.is_set():
                pause_event.clear()
                auto_paused_due_to_wifi = True
                queue_log_message("Процесс приостановлен автоматически (Wi-Fi mismatch).", "info")
            if not is_processing:
                self.start_button.setEnabled(False)
        else:
            if not is_processing:
                self.start_button.setEnabled(True)
            else:
                if auto_paused_due_to_wifi and not pause_event.is_set():
                    pause_event.set()
                    auto_paused_due_to_wifi = False
                    queue_log_message("Процесс возобновлён автоматически (Wi-Fi выбран совпадает с текущей).", "info")
                    
    # Методы для работы с файлами настроек
    def get_limit_value(self):
        try:
            return int(self.limit_entry.text())
        except ValueError:
            return 0
            
    def save_last_limit(self):
        try:
            with open(last_limit_file, "w", encoding="utf-8") as f:
                f.write(self.limit_entry.text())
        except Exception as e:
            queue_log_message(f"Ошибка при сохранении лимита: {e}", "error")
            
    def load_last_limit(self):
        try:
            with open(last_limit_file, "r", encoding="utf-8") as f:
                return f.read().strip()
        except Exception:
            return "0"
            
    def save_last_threads(self):
        """Сохранение значения количества потоков в реальном времени"""
        try:
            with open(last_threads_file, "w", encoding="utf-8") as f:
                f.write(self.threads_entry.text())
        except Exception as e:
            queue_log_message(f"Ошибка при сохранении количества потоков: {e}", "error")
            
    def load_last_threads(self):
        """Загрузка значения количества потоков"""
        try:
            with open(last_threads_file, "r", encoding="utf-8") as f:
                return f.read().strip()
        except Exception:
            return "1"  # По умолчанию 1
            
    def load_hide_browser_state(self):
        try:
            with open(hide_browser_state_file, "r", encoding="utf-8") as f:
                return f.read().strip().lower() == "true"
        except Exception:
            return False
            
    def load_run_scenario_start_state(self):
        try:
            with open(run_scenario_start_state_file, "r", encoding="utf-8") as f:
                return f.read().strip().lower() == "true"
        except Exception:
            return False
            
    def load_run_scenario_end_state(self):
        try:
            with open(run_scenario_end_state_file, "r", encoding="utf-8") as f:
                return f.read().strip().lower() == "true"
        except Exception:
            return False
            
    def save_last_model(self, model):
        try:
            with open(last_model_file, "w", encoding="utf-8") as f:
                f.write(model)
        except Exception as e:
            queue_log_message(f"Ошибка при сохранении модели: {e}", "error")
            
    def load_last_model(self):
        try:
            with open(last_model_file, "r", encoding="utf-8") as f:
                return f.read().strip()
        except Exception:
            return ""
            
    # Методы для сохранения/загрузки состояния автопрокрутки
    def save_tokens_autoscroll_state(self):
        try:
            with open(tokens_autoscroll_state_file, "w", encoding="utf-8") as f:
                f.write(str(self.tokens_autoscroll_enabled).lower())
        except Exception as e:
            queue_log_message(f"Ошибка при сохранении состояния автопрокрутки токенов: {e}", "error")
            
    def load_tokens_autoscroll_state(self):
        try:
            with open(tokens_autoscroll_state_file, "r", encoding="utf-8") as f:
                return f.read().strip().lower() == "true"
        except Exception:
            return True  # По умолчанию включено
            
    def save_failed_autoscroll_state(self):
        try:
            with open(failed_autoscroll_state_file, "w", encoding="utf-8") as f:
                f.write(str(self.failed_autoscroll_enabled).lower())
        except Exception as e:
            queue_log_message(f"Ошибка при сохранении состояния автопрокрутки неуспешных логинов: {e}", "error")
            
    def load_failed_autoscroll_state(self):
        try:
            with open(failed_autoscroll_state_file, "r", encoding="utf-8") as f:
                return f.read().strip().lower() == "true"
        except Exception:
            return True  # По умолчанию включено
            
    # Новые методы для сохранения/загрузки состояния автопрокрутки логов
    def save_log_autoscroll_state(self):
        """Сохранение состояния автопрокрутки для журнала действий"""
        try:
            with open(log_autoscroll_state_file, "w", encoding="utf-8") as f:
                f.write(str(self.log_autoscroll_enabled).lower())
        except Exception as e:
            queue_log_message(f"Ошибка при сохранении состояния автопрокрутки журнала действий: {e}", "error")
            
    def load_log_autoscroll_state(self):
        """Загрузка состояния автопрокрутки для журнала действий"""
        try:
            with open(log_autoscroll_state_file, "r", encoding="utf-8") as f:
                return f.read().strip().lower() == "true"
        except Exception:
            return True  # По умолчанию включено
    
    def closeEvent(self, event):
        """Обработка закрытия приложения"""
        try:
            # Останавливаем рабочие потоки
            self.log_worker.stop()
            self.token_worker.stop()
            self.failed_worker.stop()
            
            # Ждем завершения потоков
            self.log_worker.wait(1000)  # максимум 1 секунда
            self.token_worker.wait(1000)
            self.failed_worker.wait(1000)
            
            # Сохраняем состояния чекбоксов
            try:
                with open(hide_browser_state_file, "w", encoding="utf-8") as f:
                    f.write(str(self.hide_browser_check.isChecked()).lower())
                with open(run_scenario_start_state_file, "w", encoding="utf-8") as f:
                    f.write(str(self.run_scenario_start_check.isChecked()).lower())
                with open(run_scenario_end_state_file, "w", encoding="utf-8") as f:
                    f.write(str(self.run_scenario_end_check.isChecked()).lower())
            except Exception as e:
                print(f"Ошибка при сохранении настроек: {e}")
                    
        except Exception as e:
            print(f"Ошибка при закрытии приложения: {e}")
        finally:
            event.accept()
            
    # Методы Selenium и обработки аккаунтов
    def open_browser(self, login, password, is_retry=False, thread_index=0):
        global successful_attempts, failed_attempts, batch_success_count
        global invalid_login_count, captcha_count, token_not_found_count
        global timeout_count, element_not_found_count, unexpected_error_count, sms_code_count
        start_time = time.time()
        queue_log_message(f"Открытие браузера для: {login}{' (retry)' if is_retry else ''}", "info")

        time.sleep(thread_index * 0.5)

        options = Options()
        options.add_argument("--incognito")
        if self.hide_browser_check.isChecked():
            options.add_argument("--headless")
            options.add_argument("--disable-gpu")
            queue_log_message("Скрытый режим браузера.", "info")
        else:
            # Позиционирование только для видимых браузеров
            screen = QApplication.instance().primaryScreen().geometry()
            screen_width = screen.width()
            screen_height = screen.height()
            browser_width = screen_width // 2
            browser_height = screen_height  # Максимальная высота для всех
            y_offset = thread_index * 30  # Каждый последующий ниже на 30 px
            options.add_argument(f"--window-size={browser_width},{browser_height}")
            options.add_argument(f"--window-position=0,{y_offset}")
            queue_log_message(f"Позиционирование браузера: x=0, y={y_offset}, width={browser_width}, height={browser_height}", "info")
        service = Service(driver_path)
        driver = webdriver.Chrome(service=service, options=options)
        try:
            driver.get("https://vkhost.github.io/")
            queue_log_message("Страница vkhost открыта.", "info")

            # Ожидание полной загрузки страницы VK Host
            WebDriverWait(driver, 15).until(lambda d: d.execute_script('return document.readyState') == 'complete')
            queue_log_message("Полная загрузка страницы VK Host подтверждена.", "info")

            # Ожидание и клик на кнопку с возможным ретраем через альтернативный селектор
            found = wait_for_login_button_then_click(driver, initial_timeout=5, retry_selector='.blue-button.text-button', retry_timeout=5, main_selector="button.btn[onclick='auth(2685278)']")
            if found:
                button = WebDriverWait(driver, 15).until(
                    EC.element_to_be_clickable((By.CSS_SELECTOR, "button.btn[onclick='auth(2685278)']"))
                )
                button.click()
                queue_log_message("Кнопка авторизации нажата.", "info")
            else:
                queue_log_message("Не удалось найти кнопку авторизации даже после ретрая.", "error")
                # Здесь можно добавить обработку ошибки, например, driver.quit() и return 'failure'
                driver.quit()
                return 'failure'

            WebDriverWait(driver, 15).until(lambda d: len(d.window_handles) > 1)
            driver.switch_to.window(driver.window_handles[-1])
            queue_log_message(f"Новая вкладка: {driver.current_url}", "info")

            # Ожидание полной загрузки новой вкладки
            WebDriverWait(driver, 15).until(lambda d: d.execute_script('return document.readyState') == 'complete')
            queue_log_message("Полная загрузка новой вкладки подтверждена.", "info")

            WebDriverWait(driver, 15).until(
                lambda d: d.current_url.startswith("https://id.vk.com/auth?")
            )
            queue_log_message(f"Страница ВК: {driver.current_url}", "info")

            # Новый шаг: Нажатие на второй элемент с указанным селектором перед вводом логина
            try:
                selector_elements = WebDriverWait(driver, 15).until(
                    EC.presence_of_all_elements_located((By.CSS_SELECTOR, ".vkuiSegmentedControlOption__host.vkuiClickable__host.vkuiClickable__realClickable.vkuistyles__-focus-visible.vkuiRootComponent__host"))
                )
                if len(selector_elements) >= 2:
                    second_element = selector_elements[1]  # Второй элемент (индекс 1)
                    second_element.click()
                    queue_log_message("Нажат второй элемент по селектору перед вводом логина.", "info")
                else:
                    queue_log_message(f"Найдено меньше 2 элементов по селектору ({len(selector_elements)}), клик пропущен.", "error")
            except TimeoutException:
                queue_log_message("Timeout при поиске элементов для клика перед вводом логина.", "error")
            except Exception as e:
                short_msg = shorten_exception_message(e)
                queue_log_message(f"Ошибка при клике на второй селектор: {short_msg}", "error")

            # Шаг 1: Ввод номера в поле name="login" с выделением содержимого перед вставкой
            login_field = WebDriverWait(driver, 15).until(
                EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='login']"))
            )
            login_field.send_keys(Keys.CONTROL + 'a')  # Выделяем всё содержимое поля
            login_field.send_keys(login)  # Вставляем логин, перезаписывая выделенное
            time.sleep(0.5)  # Задержка после ввода логина
            queue_log_message("Логин вставлен после выделения поля.", "info")

            # Шаг 2: Нажатие на кнопку после ввода логина
            continue_button_login = WebDriverWait(driver, 15).until(
                EC.element_to_be_clickable((By.CSS_SELECTOR, ".vkuiInternalTappable.vkuiButton__host.vkuiButton__sizeL.vkuiButton__modePrimary.vkuiButton__appearanceAccent.vkuiButton__sizeYNone.vkuiButton__stretched.vkuiTappable__host.vkuiTappable__sizeXNone.vkuiTappable__hasPointerNone.vkuiClickable__host.vkuiClickable__realClickable.vkuistyles__-focus-visible.vkuiRootComponent__host"))
            )
            continue_button_login.click()
            queue_log_message("Кнопка после ввода логина нажата.", "info")
    
            # Ожидание полной загрузки после нажатия кнопки логина
            WebDriverWait(driver, 15).until(lambda d: d.execute_script('return document.readyState') == 'complete')
            queue_log_message("Полная загрузка после ввода логина подтверждена.", "info")

            # Шаг 3: Дождаться открытия окна с полем для пароля, SMS-кода или ошибки
            found_element = WebDriverWait(driver, 15).until(
                EC.any_of(
                    EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='password']")),  # Поле пароля
                    EC.presence_of_element_located((By.CSS_SELECTOR, ".vkc__ConfirmPhone__input")),  # Поле SMS-кода
                    EC.presence_of_element_located((By.CSS_SELECTOR, ".vkuiFormItem__bottom.vkuiFootnote__sizeYNone.vkuiFootnote__host.vkuiTypography__host.vkuiTypography__normalize.vkuiRootComponent__host"))  # Элемент ошибки
                )
            )
            element_class = found_element.get_attribute("class")
            if "vkuiFormItem__bottom" in element_class:
                error_text = "Невалидный логин или пароль"
                if is_retry:
                    error_text += " after retry"  # Добавляем "after retry" для повторной попытки
                attempt_str = " (повторная попытка)" if is_retry else " (первая попытка)"
                queue_log_message(f"Ошибка после ввода логина: {error_text}{attempt_str}", "error")
                if is_retry:
                    with global_lock:
                        invalid_login_count += 1
                        failed_attempts += 1
                    save_token_to_file(f"{error_text}: {login}", is_error=True)
                    save_failed_login(login, password)
                    append_full_result(login, password, error_text)
                    queue_token_message(f"{error_text}: {login}")
                    driver.quit()
                    return 'failure'
                else:
                    queue_log_message(f"Первичная ошибка '{error_text}' для {login}, будет ретрай", "info")
                    driver.quit()
                    return 'timeout'
            elif "vkc__ConfirmPhone__input" in element_class:
                error_text = "Код из SMS"
                with global_lock:
                    sms_code_count += 1
                    failed_attempts += 1
                save_token_to_file(f"{error_text}: {login}", is_error=True)
                save_failed_login(login, password)
                append_full_result(login, password, error_text)
                queue_log_message(f"Ошибка: {error_text}", "error")
                queue_token_message(f"{error_text}: {login}")
                driver.quit()
                return 'failure'  # Прерываем процесс, если требуется SMS-код
            # Если найдено поле пароля, продолжаем
            password_field = found_element
            password_field.send_keys(password)
            time.sleep(0.5)  # Задержка после ввода пароля
            queue_log_message("Пароль введён.", "info")

            # Шаг 4: Нажатие на кнопку после ввода пароля
            continue_button_password = WebDriverWait(driver, 15).until(
                EC.element_to_be_clickable((By.CSS_SELECTOR, ".vkuiInternalTappable.vkuiButton__host.vkuiButton__sizeL.vkuiButton__modePrimary.vkuiButton__appearanceAccent.vkuiButton__sizeYNone.vkuiButton__stretched.vkuiTappable__host.vkuiTappable__sizeXNone.vkuiTappable__hasPointerNone.vkuiClickable__host.vkuiClickable__realClickable.vkuistyles__-focus-visible.vkuiRootComponent__host"))
            )
            continue_button_password.click()
            queue_log_message("Кнопка после ввода пароля нажата.", "info")
    
            # Ожидание полной загрузки после нажатия кнопки пароля
            WebDriverWait(driver, 15).until(lambda d: d.execute_script('return document.readyState') == 'complete')
            queue_log_message("Полная загрузка после ввода пароля подтверждена.", "info")

            # Проверка на ошибки
            try:
                found_error = WebDriverWait(driver, 5).until(
                    EC.any_of(
                        EC.presence_of_element_located((By.CSS_SELECTOR, ".box_error")),
                        EC.presence_of_element_located((By.CSS_SELECTOR, ".oauth_captcha"))
                    )
                )
                error_class = found_error.get_attribute("class")
                if "box_error" in error_class:
                    error_text = "Невалидный логин или пароль"
                    if is_retry:
                        error_text += " after retry"  # Добавляем "after retry" для повторной попытки
                    attempt_str = " (повторная попытка)" if is_retry else " (первая попытка)"
                    queue_log_message(f"Ошибка: {error_text}{attempt_str}", "error")
                    if is_retry:
                        with global_lock:
                            invalid_login_count += 1
                            failed_attempts += 1
                        save_token_to_file(f"{error_text}: {login}", is_error=True)
                        save_failed_login(login, password)
                        append_full_result(login, password, error_text)
                        queue_token_message(f"{error_text}: {login}")
                        driver.quit()
                        return 'failure'
                    else:
                        queue_log_message(f"Первичная ошибка '{error_text}' для {login}, будет ретрай", "info")
                        driver.quit()
                        return 'timeout'
                elif "oauth_captcha" in error_class:
                    error_text = "Найдена капча"
                    with global_lock:
                        captcha_count += 1
                        failed_attempts += 1
                    save_token_to_file(f"{error_text}: {login}", is_error=True)
                    save_failed_login(login, password)
                    append_full_result(login, password, error_text)
                    queue_log_message(f"Ошибка: {error_text}", "error")
                    queue_token_message(f"{error_text}: {login}")
                    driver.quit()
                    return 'failure'
                else:
                    error_text = "Неизвестная ошибка"
                    if is_retry:
                        error_text += " after retry"  # Добавляем "after retry" для повторной попытки
                    attempt_str = " (повторная попытка)" if is_retry else " (первая попытка)"
                    queue_log_message(f"Ошибка: {error_text}{attempt_str}", "error")
                    if is_retry:
                        with global_lock:
                            unexpected_error_count += 1
                            failed_attempts += 1
                        save_token_to_file(f"{error_text}: {login}", is_error=True)
                        save_failed_login(login, password)
                        append_full_result(login, password, error_text)
                        queue_token_message(f"{error_text}: {login}")
                        driver.quit()
                        return 'failure'
                    else:
                        queue_log_message(f"Первичная ошибка '{error_text}' для {login}, будет ретрай", "info")
                        driver.quit()
                        return 'timeout'
            except TimeoutException:
                pass

            # Шаг 5: Ожидание страницы с токеном
            WebDriverWait(driver, 15).until(
                lambda d: d.current_url.startswith("https://oauth.vk.com/blank.html#access_token=")
            )
            url = driver.current_url
            queue_log_message(f"Страница с токеном открыта: {url}", "info")
    
            # Ожидание полной загрузки страницы с токеном
            WebDriverWait(driver, 15).until(lambda d: d.execute_script('return document.readyState') == 'complete')
            queue_log_message("Полная загрузка страницы с токеном подтверждена.", "info")

            # Шаг 6: Извлечение токена
            token_match = re.search(r"access_token=([^&]+)", url)
            if token_match:
                token = token_match.group(1)
                queue_log_message(f"Токен успешно извлечён: {token}", "success")
                save_token_to_file(token)
                append_full_result(login, password, token)
                with global_lock:
                    successful_attempts += 1
                    batch_success_count += 1
                queue_log_message("Токен получен и сохранён.", "success")
                driver.quit()
                return 'success'
            else:
                with global_lock:
                    failed_attempts += 1
                    token_not_found_count += 1
                error_text = f"Токен не найден в URL: {url}"
                queue_log_message(error_text, "error")
                save_token_to_file(error_text, is_error=True)
                save_failed_login(login, password)
                append_full_result(login, password, "Токен не найден")
                queue_token_message(f"Токен не найден: {login}")
                driver.quit()
                return 'failure'

        except TimeoutException:
            attempt_str = " (повторная попытка)" if is_retry else " (первая попытка)"
            queue_log_message(f"Ошибка: Timeout. URL: {driver.current_url}{attempt_str}", "error")

            if is_retry:
                with global_lock:
                    timeout_count += 1
                    failed_attempts += 1
                save_token_to_file(f"Timeout after retry: {login}", is_error=True)
                save_failed_login(login, password)
                append_full_result(login, password, "Timeout after retry")
                queue_token_message(f"Timeout after retry: {login}")
            else:
                queue_log_message(f"Первичный timeout для {login}, будет ретрай", "info")

            driver.quit()
            return 'timeout'

        except NoSuchElementException:
            with global_lock:
                failed_attempts += 1
                element_not_found_count += 1
            save_token_to_file(f"Элемент не найден: {login}", is_error=True)
            save_failed_login(login, password)
            append_full_result(login, password, "Элемент не найден")
            queue_log_message("Ошибка: Элемент не найден.", "error")
            queue_token_message(f"Элемент не найден: {login}")
            driver.quit()
            return 'failure'

        except Exception as e:
            error_text = "Неожиданная ошибка"
            if is_retry:
                error_text += " after retry"
            attempt_str = " (повторная попытка)" if is_retry else " (первая попытка)"
            short_msg = shorten_exception_message(e)
            queue_log_message(f"{error_text}: {short_msg}{attempt_str}", "error")

            if is_retry:
                with global_lock:
                    unexpected_error_count += 1
                    failed_attempts += 1
                save_token_to_file(f"{error_text}: {login}", is_error=True)  # Сохраняем без полного e
                save_failed_login(login, password)
                append_full_result(login, password, error_text)
                queue_token_message(f"{error_text}: {login}")
                driver.quit()
                return 'failure'
            else:
                queue_log_message(f"Первичная '{error_text}' для {login}, будет ретрай", "info")
                driver.quit()
                return 'timeout'

        finally:
            # Driver.quit() уже в блоках, но на всякий случай
            try:
                driver.quit()
            except:
                pass
            queue_log_message("Браузер закрыт.", "info")
            signals.update_labels.emit()
            end_time = time.time()
            processing_time = end_time - start_time
            processing_time_formatted = format_time(processing_time)
            queue_log_message(f"Обработка аккаунта {login} завершена за {processing_time_formatted}.", "info")

    def run_flight_mode_scenario(self):
        chosen_model = self.device_combobox.currentText()
        if not chosen_model:
            queue_log_message("Не выбрана модель для сценария.", "error")
            return
            
        serial = devices_by_model.get(chosen_model)
        if not serial:
            queue_log_message(f"Не найден serial для '{chosen_model}'.", "error")
            return
            
        try:
            queue_log_message(f"Запуск сценария на '{chosen_model}'.", "info")
            d = u2.connect(serial)
            queue_log_message(f"Устройство: {d.device_info}", "info")
            
            subprocess.run(["adb", "-s", serial, "shell", "am", "start", "-a", "android.settings.AIRPLANE_MODE_SETTINGS"])
            time.sleep(0.3)
            
            switch_element = d(resourceId="android:id/switch_widget")
            switch_element.click_exists(timeout=3)
            time.sleep(0.3)
            switch_element.click_exists(timeout=3)
            time.sleep(0.5)
            
            d.app_start("com.android.settings", ".TetherSettings")
            time.sleep(0.3)
            if d(resourceId="com.android.settings:id/recycler_view").child(index=0).exists(timeout=2):
                d(resourceId="com.android.settings:id/recycler_view").child(index=0).click()
                time.sleep(0.3)
                
            for _ in range(3):
                d.press("back")
                time.sleep(0.2)
                
            queue_log_message("Ожидание Wi-Fi на ПК...", "info")
            while not is_connected():
                time.sleep(0.3)
            queue_log_message("Wi-Fi подключен.", "info")
            time.sleep(1)
            
        except Exception as e:
            queue_log_message(f"Ошибка сценария: {e}", "error")

# Вспомогательные функции
def append_full_result(login, password, result):
    with global_lock:
        all_results.append((login, password, result))

def add_token_separator():
    try:
        with file_lock:
            with open(tokens_file, "a", encoding="utf-8") as f:
                now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                separator = f"\n{'='*30} {now} {'='*30}\n"
                f.write(separator)
    except Exception as e:
        queue_log_message(f"Ошибка при добавлении разделителя: {e}", "error")

def save_token_to_file(token, is_error=False):
    try:
        with file_lock:
            with open(tokens_file, "a", encoding='utf-8') as file:
                if is_error:
                    file.write(f"Ошибка: {token}\n")
                else:
                    file.write(token + "\n")
        queue_log_message(f"{'Ошибка' if is_error else 'Токен'} сохранён: {tokens_file}", "info")
        if not is_error:
            queue_token_message(token)
    except Exception as e:
        queue_log_message(f"Ошибка сохранения токена: {e}", "error")

def save_failed_login(login, password):
    try:
        with file_lock:
            with open(failed_logins_file, "a", encoding='utf-8') as file:
                file.write(f"{login}:{password}\n")
        queue_log_message(f"Неуспешный логин сохранён: {failed_logins_file}", "info")
        queue_failed_message(login, password)
    except Exception as e:
        queue_log_message(f"Ошибка сохранения логина: {e}", "error")

def read_login_passwords():
    if not os.path.exists(login_pass_file):
        queue_log_message(f"Файл не найден: {login_pass_file}", "error")
        return []
    try:
        with open(login_pass_file, "r", encoding='utf-8') as file:
            accounts = [line.strip().split(":", 1) for line in file if line.strip()]
        return accounts
    except Exception as e:
        queue_log_message(f"Ошибка чтения файла: {e}", "error")
        return []

def is_connected():
    try:
        socket.create_connection(("8.8.8.8", 53), timeout=2)
        return True
    except OSError:
        return False

def get_connected_wifi():
    try:
        output = subprocess.check_output("netsh wlan show interfaces", shell=True, encoding="utf-8")
        for line in output.splitlines():
            if "SSID" in line and "BSSID" not in line:
                parts = line.split(":", 1)
                if len(parts) > 1:
                    ssid = parts[1].strip()
                    return ssid if ssid else "Нет подключения"
        return "Нет подключения"
    except Exception as e:
        return f"Ошибка: {e}"
        
def get_available_networks():
    networks = []
    try:
        output = subprocess.check_output("netsh wlan show networks", shell=True, encoding="utf-8")
        for line in output.splitlines():
            if "SSID" in line and ":" in line:
                parts = line.split(":", 1)
                if len(parts) > 1:
                    ssid = parts[1].strip()
                    if ssid and ssid not in networks:
                        networks.append(ssid)
    except Exception as e:
        queue_log_message(f"Ошибка при получении сетей: {e}", "error")
    return networks

def connect_to_wifi(ssid):
    """Подключение к выбранной Wi-Fi сети"""
    try:
        # Предполагается, что сеть уже известна системе (профиль существует)
        subprocess.run(f'netsh wlan connect name="{ssid}" ssid="{ssid}"', shell=True, check=True)
        time.sleep(5)  # Ожидание подключения
        if get_connected_wifi() == ssid:
            return True
        else:
            return False
    except subprocess.CalledProcessError as e:
        queue_log_message(f"Ошибка при подключении к Wi-Fi '{ssid}': {e}", "error")
        return False
    except Exception as e:
        queue_log_message(f"Неизвестная ошибка при подключении к Wi-Fi '{ssid}': {e}", "error")
        return False

def get_external_ip():
    try:
        response = requests.get('https://api.ipify.org', timeout=5)
        response.raise_for_status()
        return response.text.strip()
    except requests.RequestException:
        return "Недоступно"

def get_location_by_ip(ip):
    try:
        response = requests.get(f'https://ipinfo.io/{ip}/json', timeout=5)
        response.raise_for_status()
        data = response.json()
        city = data.get('city', 'Неизвестно')
        region = data.get('region', 'Неизвестно')
        country = data.get('country', 'Неизвестно')
        return f"{city}, {region}, {country}"
    except requests.RequestException:
        return "Недоступно"

def main():
    app = QApplication(sys.argv)
    
    # Установка стиля приложения
    app.setStyle('Fusion')
    
    window = MainWindow()
    window.show()
    
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()

